feat(core): cross-chain dependency refs — resolve an arg to a contract deployed on another network (#159) - #172
Merged
Conversation
…part 1 (issue #159) Adds `{ kind: "crossRef", network, contract }` to the ContractArg union: a reference to a contract deployed on a DIFFERENT network, resolved to a literal address at deploy time (part 2 of this feature). Shape-validated by zod (non-empty network/contract) and deliberately excluded from validateSpec's ref/cycle checks — the target lives outside this spec's own id space, so MISSING_REF/SELF_REFERENCE/CYCLE checks would be meaningless. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
compileSpec() must never see a crossRef arg — they are pre-resolved to
literals before compilation (see the next commit). Mirrors the existing
UNRESOLVED_RESOLVER_ARG guard: mapContractArg() throws
CompileError("UNRESOLVED_CROSS_REF_ARG") for a direct caller who bypasses
the pre-resolution pass. buildCreationOrder() already excludes crossRef from
build-order edges (only "ref"/"expr" are handled there) — added tests
proving no phantom dependency is introduced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n (issue #159) Extracts deploy.ts's private journal-address-read helper into a reusable resolve/journal.ts (loadAddressesFromJournal) — a pure extraction, same Ignition status() + stripModulePrefix behavior. Adds resolve/crossRef.ts's resolveCrossRefArgs(), which walks a spec and replaces every `{ kind: "crossRef" }` arg (constructor args, upgradeable.initializer.args, upgradeable.proxyAdminOwner) with a literal address read from the target network's journal (via an injected ResolveCrossRefOptions.journals map — core has no network registry of its own). Throws CrossRefError with CROSS_REF_UNKNOWN_NETWORK or CROSS_REF_NOT_DEPLOYED on failure. Tests build real journals via the in-memory fake EIP-1193 provider (no anvil needed) to exercise the actual Ignition status() read path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…159) deploy() gains DeployOptions.crossNetworkJournals: an injected map of network name -> journal location. When the spec has crossRef args, deploy() resolves them (via resolveCrossRefArgs) BEFORE the existing resolver pre-resolution pass — both are pre-compile literal substitutions, ordered crossRef -> resolver. CrossRefError is caught and re-wrapped as DeployError("CROSS_REF_ERROR"), mirroring the ResolveError wrapping already in place. Fully backward compatible: specs with no crossRef args touch zero extra I/O. simulate()'s PlannedStep gains a distinct `crossRefs` field (network + contract pairs) — kept separate from `dependsOn` since a crossRef targets a different network's deployment, never a same-run build/deploy-order dependency. Studio rendering of cross-network edges is a follow-up. Exports CrossRefArg, crossRefArgSchema, CrossNetworkJournal, ResolveCrossRefOptions, resolveCrossRefArgs, specHasCrossRefArgs, CrossRefError, and CrossRefErrorCode from the package root. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…(issue #159) Mirror the existing Object.hasOwn guard on options.journals[network] with the same guard on the loaded addresses[contract] map, so a contract id equal to a prototype key (e.g. "__proto__", "constructor", "toString") throws the normal CrossRefError("CROSS_REF_NOT_DEPLOYED") instead of silently resolving to an inherited Object.prototype member. Also builds the address map in loadAddressesFromJournal with Object.create(null) for defense in depth. Additionally validate the resolved value with viem's isAddress before substituting it as a { kind: "literal" } constructor arg, so a journal that somehow yields a non-address string can never be injected into on-chain calldata — refusing with the same CROSS_REF_NOT_DEPLOYED error instead.
robercano
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #159.
What
Adds a cross-chain dependency ref to the
@redeploy/corespec: a contract targeting network B can declare a constructor arg that resolves to the address of a contract already deployed on network A (e.g. an L2 bridge adapter taking its L1 counterpart's address). This adds the cross-network edge to the spec graph on top of the existing multi-network / per-network-journal machinery.How
CrossRefArg { kind: "crossRef", network, contract }added to theContractArgunion, with a matching zodcrossRefArgSchema(both fields non-empty). Validation is shape-only — a crossRef is deliberately not checked against same-spec ids (no falseMISSING_REF/SELF_REFERENCE), contributes no intra-spec ordering edge (detectCycles/buildCreationOrder), and is excluded fromsimulate'sdependsOn.resolveCrossRefArgs(spec, { journals })(mirroring the existingresolveSpecResolverArgs) reads the referenced network's Ignition journal via core's ownstatus()primitive and substitutes the resolved address as a{ kind: "literal" }arg before compile. The journal-reading helper (loadResolvedAddressesFromJournal) was extracted into a reusableresolve/journal.ts#loadAddressesFromJournalanddeploy.tsrefactored to call it (behavior-identical).status()— it does not import@redeploy/reader(core is the base package; reader builds on core). The network→journal-dir map is injected by the caller (DeployOptions.crossNetworkJournals), so core never owns a network registry.CrossRefError("CROSS_REF_UNKNOWN_NETWORK")when the referenced network isn't in the injected map;CrossRefError("CROSS_REF_NOT_DEPLOYED")when the source contract isn't in that network's journal yet (or the journal is missing) — re-wrapped asDeployError("CROSS_REF_ERROR")and thrown before any transaction is broadcast. Cross-chain ordering is the operator's responsibility in v1 (no cross-chain orchestrator, by design).compileSpecthrowsUNRESOLVED_CROSS_REF_ARGif an unresolved crossRef ever reaches it (across constructor args,upgradeable.initializer.args, andproxyAdminOwner), so a crossRef can never silently reach Ignition.PlannedStepgains a distinctcrossRefsfield so plan/diff consumers can surface cross-network edges distinctly (studio rendering is a follow-up).Object.hasOwn-guarded (and the map built withObject.create(null)) so acontractid equal to a prototype key can't bypass the not-deployed hard error; the resolved value isisAddress-validated before it's injected as calldata.Backward compatibility
Fully backward compatible: specs with no crossRef args do zero extra work and no extra I/O (short-circuit via
specHasCrossRefArgs), and the single-network resolver path is unchanged.Tests
~57 new vitest cases across
crossRef,crossRefAddressGuard,deploy,simulate,compile, andspectest files, including real-journal resolution (source network seeded via a genuinedeploy()over an in-memory EIP-1193 fake — no anvil), both hard-error paths asserting zero tx sent before failure, prototype-keycontract/networkguards, invalid-address guard, all three arg positions, and backward-compat.@redeploy/corecoverage 94% (threshold 80%). Repo-widepnpm -r build+pnpm -r typecheckpass (all downstream packages compile against the new exports).Review
Reviewed adversarially through 4 lenses (correctness, tests, security, performance) — all approved; the one cross-cutting hardening finding (prototype-pollution guard on the address lookup) is applied in this branch.
Follow-ups (intentionally out of scope — core-only PR)
@redeploy/deploy-server: wirecrossNetworkJournalsfrom itsNetworksRegistry(per-networkdeploymentDir).@redeploy/studio: render cross-network edges fromPlannedStep.crossRefs.@redeploy/readeraddress-book integration as an alternative journal source (must stay caller-side to preserve the core→reader dependency direction).🤖 Generated with Claude Code